package com.squeekeek.util.qrcode.Core;

import java.util.ArrayList;
import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;

public class QRFinderPatternFinder {
	public static final int SCAN_ROW_MAJOR = 0x01;
	public static final int SCAN_COL_MAJOR = 0x02;
	
	/**
	 * <p>
	 * Gets a list of centers that point to squares having the finder ratio.
	 * </p>
	 * <p>
	 * This function scans the image 
	 * </p>
	 * 
	 * @param inImg Image matrix to process
	 * @param scanDim Type of scan, either by rows first or by columns first
	 * @return A list of center points that point to squares having the
	 * finder ratio
	 * @see #scanImage	
	 */
	public static List <Point> getFinderPatterns(Mat inImg, int scanDim) {
		List <Point> centerPoints;
		
		// Scan rows for the finder pattern ratio. Scan columns afterwards
		centerPoints = scanImage(inImg, 0, inImg.rows(), 0, inImg.cols(), scanDim, true);
		centerPoints = scanImageDiag(inImg, centerPoints);
		
		return centerPoints;
	}
	
	public static void drawOverPatterns(Mat inImg, QRFinderPattern finderCenters) {
		Point[] points = finderCenters.getArray();
		
		for(int i = 0; i < 3; i++) {
			Core.circle(inImg, points[i], 5, new Scalar(0x99), -1);
			Core.line(inImg, points[i], points[(i + 1) % 3], new Scalar(0x55), 5);
		}
	}
	
	/**
	 * <p>
	 * Regroups a list of points into a finder pattern class.
	 * </p>
	 * <p>
	 * The function regroups a list of points that approximate an isosceles
	 * triangle into a class. The class contains three finder pattern locations
	 * that are initially not oriented. The finder patterns should later be oriented
	 * using {@link QRImageCorrection#reorderOrientation}.
	 * </p>
	 * 
	 * @param finderPoints A list of center points that point to a block
	 * approximating the finder ratio
	 * @return A list of 3-finder pattern clusters representing a QR Code
	 * @see #getFinderPatterns
	 * @see QRFinderPattern
	 */
	public static List <QRFinderPattern> getQRPoints(List <Point> finderPoints) {
		int numEqualSides;
		Mat sideAngles;
		List <QRFinderPattern> finderPatterns = new ArrayList <QRFinderPattern> ();
		
		if(finderPoints.size() == 3) {
			finderPatterns.add(new QRFinderPattern(finderPoints, false));
		}
		else if(finderPoints.size() < 3) {
			return finderPatterns;
		}
		else {
			for(int i = 0; i < finderPoints.size(); i++) {
				for(int j = i + 1; j < finderPoints.size(); j++) {
					for(int k = j + 1; k < finderPoints.size(); k++) {
						// Check for an approximate isosceles triangle
						numEqualSides = 0;
						sideAngles = OpenCVExtras.lawOfCosines(finderPoints.get(i), finderPoints.get(j), finderPoints.get(k)).col(1);
						
						// Check for equal acute angles
						for(int r = 0; r < 3; r++) {
							double angle1 = sideAngles.get(r, 0)[0];
							double angle2 = sideAngles.get((r + 1) % 3, 0)[0];
							double angle3 = sideAngles.get((r + 2) % 3, 0)[0];
							//
							if( (int) angle1 <= 90 &&
									(int) angle2 <= 90 &&
									(int) Math.abs(angle3 - 90) <= 10 &&
									(int) (Math.abs(angle1 - angle2)) <= 5 ) {
								numEqualSides++;
							}
						}
						
						if(numEqualSides == 1) {
							finderPatterns.add(new QRFinderPattern(finderPoints.get(i), finderPoints.get(j), finderPoints.get(k)));
						}
					}
				}
			}
		}
		
		return finderPatterns;
	}
	
	/**
	 * Scans an image for QR Code finder patterns in a certain bounded rectangle
	 * area either by row or by column.
	 * <br><br>
	 * The function takes in an input image of type <code>OpenCV.Mat inImg</code>
	 * which is scanned for finder patterns in the rectangle bounded by
	 * <code>x elementof [rowStart, rowEnd), y elementof [colStart, colEnd)</code>.
	 * <code>scanType</code> determines whether to scan for pixels in each row
	 * or for pixels in each column. This value can only be either
	 * <code>SCAN_ROW_MAJOR</code> or <code>SCAN_COL_MAJOR</code>. <code>scanOther</code>
	 * determines whether to scan in the other dimension. For example, if
	 * <code>scanType</code> is in <code>SCAN_ROW_MAJOR</code> and
	 * <code>scanOther</code> is set to <code>true</code>, it scans for the other
	 * dimension in <code>SCAN_COL_MAJOR</code> and returns the centers of the
	 * prospective finder points. If <code>scanOther</code> is false in this example,
	 * it returns a range of coordinates <code>[range.x, range.y)</code> of type
	 * <code>OpenCV.Point</code> where the finder ratio is found in
	 * <code>SCAN_COL_MAJOR</code> dimension.
	 * 
	 * @param inImg Image matrix to process for finder patterns
	 * @param rowStart Start of row to scan
	 * @param rowEnd End of row to scan
	 * @param colStart Start of column to scan
	 * @param colEnd End of column to scan
	 * @param scanType Type of scan, either by rows first or by columns first
	 * @param scanOther Set to true if the other dimension should be scanned
	 * @return If <code>scanOther</code> is true, it returns the centers of the finder
	 * patterns. Else, it returns a range of coordinates scanned in the other dimension
	 * denoted by <code>scanType</code>
	 */
	private static List <Point> scanImage(Mat inImg, int rowStart, int rowEnd, int colStart, int colEnd, int scanType, boolean scanOther) {
		int pixVal;
		int majorStart, majorEnd;
		int minorStart, minorEnd;
		int totalPixels;
		int currentState = 0;
		int[] statePixels = {0, 0, 0, 0, 0};
		double majorCenter, minorCenter;
		Point foundFinderCenter;
		List <Point> scanRanges;
		List <Point> centerPoints = new ArrayList <Point> ();
		
		switch(scanType) {
		case SCAN_ROW_MAJOR:
			majorStart = rowStart;
			majorEnd = rowEnd;
			minorStart = colStart;
			minorEnd = colEnd;
			break;
		case SCAN_COL_MAJOR:
			majorStart = colStart;
			majorEnd = colEnd;
			minorStart = rowStart;
			minorEnd = rowEnd;
			break;
		default:
			majorStart = rowStart;
			majorEnd = rowEnd;
			minorStart = colStart;
			minorEnd = colEnd;
		}
		
		for(int i = majorStart; i < majorEnd; i += 5) {
			for(int j = minorStart; j < minorEnd; j++) {
				if(scanType == SCAN_COL_MAJOR) {
					pixVal = (int) inImg.get(j, i)[0];
				}
				else {
					pixVal = (int) inImg.get(i, j)[0];
				}
				
				if((currentState == 4) && (pixVal == QRBinarize.PIXEL_WHITE)) {
					if(checkFinderRatio(statePixels)) {
						totalPixels = 0;
						
						for(int k : statePixels) {
							totalPixels += k;
						}
						
						if(scanOther) {
							if(scanType == SCAN_ROW_MAJOR) {
								minorCenter = j - totalPixels + totalPixels / 2;
								
								scanRanges = scanImage(inImg, 0, inImg.rows(), (int) minorCenter, (int) (minorCenter + 1), SCAN_COL_MAJOR, false);
								
								for(Point iRange : scanRanges) {
									if((int) iRange.x <= i && i <= (int) iRange.y) {
										majorCenter = iRange.x + (iRange.y - iRange.x) / 2;

										foundFinderCenter = new Point(minorCenter, majorCenter);
										
										if(isUniqueCenter(centerPoints, foundFinderCenter)) {
											centerPoints.add(foundFinderCenter);
										}
									}
								}
							}
							else if(scanType == SCAN_COL_MAJOR) {
								minorCenter = j - totalPixels + totalPixels / 2;
								
								scanRanges = scanImage(inImg, (int) minorCenter, (int) (minorCenter + 1), 0, inImg.cols(), SCAN_ROW_MAJOR, false);								
								
								// Check column ranges
								for(Point iRange : scanRanges) {
									if((int) iRange.x <= i && i <= (int) iRange.y) {
										majorCenter = iRange.x + (iRange.y - iRange.x) / 2;

										foundFinderCenter = new Point(majorCenter, minorCenter);
										
										if(isUniqueCenter(centerPoints, foundFinderCenter)) {
											centerPoints.add(foundFinderCenter);
										}
									}
								}
							}
						}
						else {
							centerPoints.add(new Point(j - totalPixels, j));
						}
						
						// Revert to state 1 since we have found a prospective
						// finder pattern and we're already counting white
						// pixels. This makes checkFinderRatio() return false
						currentState = 1;
						
						for(int k = 0; k < 5; k++) {
							statePixels[k] = 0;
						}
					}
					else {
						// Revert to the state where we just finished counting
						// the supposed center black square.
						currentState = 3;
						
						statePixels[0] = statePixels[2];
						statePixels[1] = statePixels[3];
						statePixels[2] = statePixels[4];
						statePixels[3] = 0;
						statePixels[4] = 0;
					}
				}
				else if((currentState == 0 || currentState == 2) && (pixVal == QRBinarize.PIXEL_WHITE)) {
					currentState++;
				}
				else if((currentState == 1 || currentState == 3) && (pixVal == QRBinarize.PIXEL_BLACK)) {
					currentState++;
				}
				
				// Increment current pixels
				statePixels[currentState]++;
			}
			
			// Revert to the clear state since we just finished scanning a
			// row or column
			currentState = 0;
			
			for(int k = 0; k < 5; k++) {
				statePixels[k] = 0;
			}
		}
		
		return centerPoints;
	}
	
	private static List <Point> scanImageDiag(Mat inImg, List <Point> centers) {
		int rowStart;
		int colStart;
		int currentState;
		int pixVal;
		int[] statePixels = {0, 0, 0, 0, 0};
		List <Point> newCenters = new ArrayList <Point> ();
		
		for(Point iPoint : centers) {
			if(iPoint.x < iPoint.y) {
				rowStart = (int) (iPoint.y - iPoint.x);
				colStart = 0;
			}
			else if(iPoint.x > iPoint.y) {
				rowStart = 0;
				colStart = (int) (iPoint.x - iPoint.y);
			}
			else {
				rowStart = 0;
				colStart = 0;
			}
			
			currentState = 0;
			
			int j = colStart;
			
			innerLoop:
			for(int i = rowStart; i < inImg.rows() && j < inImg.cols(); i++) {
				pixVal = (int) inImg.get(i, j)[0];
				
				if((currentState == 4) && (pixVal == QRBinarize.PIXEL_WHITE)) {
					if(checkFinderRatio(statePixels)) {
						newCenters.add(iPoint);
						break innerLoop;
					}
					else {
						// Revert to the state where we just finished counting
						// the supposed center black square.
						currentState = 3;
						
						statePixels[0] = statePixels[2];
						statePixels[1] = statePixels[3];
						statePixels[2] = statePixels[4];
						statePixels[3] = 1;
						statePixels[4] = 0;
					}
				}
				else if((currentState == 0 || currentState == 2) && (pixVal == QRBinarize.PIXEL_WHITE)) {
					currentState++;
				}
				else if((currentState == 1 || currentState == 3) && (pixVal == QRBinarize.PIXEL_BLACK)) {
					currentState++;
				}
				
				statePixels[currentState]++;
				j++;
			}
		}
		
		return centers;
	}
	
	private static boolean checkFinderRatio(int[] statePixels) {
		int totalPixels = 0;
		double modSize, modRatio;
		double maxVar;
		
		for(int i : statePixels) {
			if(i < 1) {
				return false;
			}
			
			totalPixels += i;
		}
		
		if(totalPixels < 7) {
			return false;
		}
		
		modSize = totalPixels / 7.0;
		maxVar = modSize / 2.0;
		
		for(int i = 0; i < 5; i++) {
			if(i == 2) {
				modRatio = 3 * modSize;
			}
			else {
				modRatio = modSize;
			}
			
			if((int) (1000 * Math.abs(modRatio - statePixels[i])) > (int) (1000 * maxVar)) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Checks whether a point is already included in a list of points
	 * <br><br>
	 * The function checks whether the list of <code>OpenCV.Point points</code>
	 * already contains the point <code>OpenCV.Point singlePoint</code> to a tolerance
	 * of 5 pixels.
	 * 
	 * @param points The list of points
	 * @param singlePoint A single point to check
	 * @return Returns <code>true</code> if the point is not yet found in the
	 * given list of points
	 */
	private static boolean isUniqueCenter(List <Point> points, Point singlePoint) {
		for(Point iPoint : points) {
			if((int) (Math.abs(iPoint.x - singlePoint.x)) < 5 && (int) (Math.abs(iPoint.y - singlePoint.y)) < 5) {
				return false;
			}
		}
		
		return true;
	}
}
